/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.sun.org.apache.xml.internal.security.keys.keyresolver.implementations;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.sun.org.apache.xml.internal.security.c14n.CanonicalizationException;
import com.sun.org.apache.xml.internal.security.exceptions.XMLSecurityException;
import com.sun.org.apache.xml.internal.security.keys.content.RetrievalMethod;
import com.sun.org.apache.xml.internal.security.keys.content.x509.XMLX509Certificate;
import com.sun.org.apache.xml.internal.security.keys.keyresolver.KeyResolver;
import com.sun.org.apache.xml.internal.security.keys.keyresolver.KeyResolverException;
import com.sun.org.apache.xml.internal.security.keys.keyresolver.KeyResolverSpi;
import com.sun.org.apache.xml.internal.security.keys.storage.StorageResolver;
import com.sun.org.apache.xml.internal.security.signature.XMLSignatureInput;
import com.sun.org.apache.xml.internal.security.transforms.Transforms;
import com.sun.org.apache.xml.internal.security.utils.Constants;
import com.sun.org.apache.xml.internal.security.utils.XMLUtils;
import com.sun.org.apache.xml.internal.security.utils.resolver.ResourceResolver;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * The RetrievalMethodResolver can retrieve public keys and certificates from
 * other locations. The location is specified using the ds:RetrievalMethod
 * element which points to the location. This includes the handling of raw
 * (binary) X.509 certificate which are not encapsulated in an XML structure.
 * If the retrieval process encounters an element which the
 * RetrievalMethodResolver cannot handle itself, resolving of the extracted
 * element is delegated back to the KeyResolver mechanism.
 *
 * @author $Author: raul $ modified by Dave Garcia
 */
public class RetrievalMethodResolver extends KeyResolverSpi {

  /**
   * {@link org.apache.commons.logging} logging facility
   */
  private static java.util.logging.Logger log =
      java.util.logging.Logger.getLogger(RetrievalMethodResolver.class.getName());

  /**
   * Method engineResolvePublicKey
   *
   * @inheritDoc
   */
  public PublicKey engineLookupAndResolvePublicKey(
      Element element, String baseURI, StorageResolver storage
  ) {
    if (!XMLUtils.elementIsInSignatureSpace(element, Constants._TAG_RETRIEVALMETHOD)) {
      return null;
    }

    try {
      // Create a retrieval method over the given element
      RetrievalMethod rm = new RetrievalMethod(element, baseURI);
      String type = rm.getType();
      XMLSignatureInput resource = resolveInput(rm, baseURI, secureValidation);
      if (RetrievalMethod.TYPE_RAWX509.equals(type)) {
        // a raw certificate, direct parsing is done!
        X509Certificate cert = getRawCertificate(resource);
        if (cert != null) {
          return cert.getPublicKey();
        }
        return null;
      }
      Element e = obtainReferenceElement(resource);

      // Check to make sure that the reference is not to another RetrievalMethod
      // which points to this element
      if (XMLUtils.elementIsInSignatureSpace(e, Constants._TAG_RETRIEVALMETHOD)) {
        if (secureValidation) {
          String error = "Error: It is forbidden to have one RetrievalMethod "
              + "point to another with secure validation";
          if (log.isLoggable(java.util.logging.Level.FINE)) {
            log.log(java.util.logging.Level.FINE, error);
          }
          return null;
        }
        RetrievalMethod rm2 = new RetrievalMethod(e, baseURI);
        XMLSignatureInput resource2 = resolveInput(rm2, baseURI, secureValidation);
        Element e2 = obtainReferenceElement(resource2);
        if (e2 == element) {
          if (log.isLoggable(java.util.logging.Level.FINE)) {
            log.log(java.util.logging.Level.FINE,
                "Error: Can't have RetrievalMethods pointing to each other");
          }
          return null;
        }
      }

      return resolveKey(e, baseURI, storage);
    } catch (XMLSecurityException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "XMLSecurityException", ex);
      }
    } catch (CertificateException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "CertificateException", ex);
      }
    } catch (IOException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "IOException", ex);
      }
    } catch (ParserConfigurationException e) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "ParserConfigurationException", e);
      }
    } catch (SAXException e) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "SAXException", e);
      }
    }
    return null;
  }

  /**
   * Method engineResolveX509Certificate
   *
   * @inheritDoc
   */
  public X509Certificate engineLookupResolveX509Certificate(
      Element element, String baseURI, StorageResolver storage) {
    if (!XMLUtils.elementIsInSignatureSpace(element, Constants._TAG_RETRIEVALMETHOD)) {
      return null;
    }

    try {
      RetrievalMethod rm = new RetrievalMethod(element, baseURI);
      String type = rm.getType();
      XMLSignatureInput resource = resolveInput(rm, baseURI, secureValidation);
      if (RetrievalMethod.TYPE_RAWX509.equals(type)) {
        return getRawCertificate(resource);
      }

      Element e = obtainReferenceElement(resource);

      // Check to make sure that the reference is not to another RetrievalMethod
      // which points to this element
      if (XMLUtils.elementIsInSignatureSpace(e, Constants._TAG_RETRIEVALMETHOD)) {
        if (secureValidation) {
          String error = "Error: It is forbidden to have one RetrievalMethod "
              + "point to another with secure validation";
          if (log.isLoggable(java.util.logging.Level.FINE)) {
            log.log(java.util.logging.Level.FINE, error);
          }
          return null;
        }
        RetrievalMethod rm2 = new RetrievalMethod(e, baseURI);
        XMLSignatureInput resource2 = resolveInput(rm2, baseURI, secureValidation);
        Element e2 = obtainReferenceElement(resource2);
        if (e2 == element) {
          if (log.isLoggable(java.util.logging.Level.FINE)) {
            log.log(java.util.logging.Level.FINE,
                "Error: Can't have RetrievalMethods pointing to each other");
          }
          return null;
        }
      }

      return resolveCertificate(e, baseURI, storage);
    } catch (XMLSecurityException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "XMLSecurityException", ex);
      }
    } catch (CertificateException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "CertificateException", ex);
      }
    } catch (IOException ex) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "IOException", ex);
      }
    } catch (ParserConfigurationException e) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "ParserConfigurationException", e);
      }
    } catch (SAXException e) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "SAXException", e);
      }
    }
    return null;
  }

  /**
   * Retrieves a x509Certificate from the given information
   */
  private static X509Certificate resolveCertificate(
      Element e, String baseURI, StorageResolver storage
  ) throws KeyResolverException {
    if (log.isLoggable(java.util.logging.Level.FINE)) {
      log.log(java.util.logging.Level.FINE, "Now we have a {" + e.getNamespaceURI() + "}"
          + e.getLocalName() + " Element");
    }
    // An element has been provided
    if (e != null) {
      return KeyResolver.getX509Certificate(e, baseURI, storage);
    }
    return null;
  }

  /**
   * Retrieves a PublicKey from the given information
   */
  private static PublicKey resolveKey(
      Element e, String baseURI, StorageResolver storage
  ) throws KeyResolverException {
    if (log.isLoggable(java.util.logging.Level.FINE)) {
      log.log(java.util.logging.Level.FINE, "Now we have a {" + e.getNamespaceURI() + "}"
          + e.getLocalName() + " Element");
    }
    // An element has been provided
    if (e != null) {
      return KeyResolver.getPublicKey(e, baseURI, storage);
    }
    return null;
  }

  private static Element obtainReferenceElement(XMLSignatureInput resource)
      throws CanonicalizationException, ParserConfigurationException,
      IOException, SAXException, KeyResolverException {
    Element e;
    if (resource.isElement()) {
      e = (Element) resource.getSubNode();
    } else if (resource.isNodeSet()) {
      // Retrieved resource is a nodeSet
      e = getDocumentElement(resource.getNodeSet());
    } else {
      // Retrieved resource is an inputStream
      byte inputBytes[] = resource.getBytes();
      e = getDocFromBytes(inputBytes);
      // otherwise, we parse the resource, create an Element and delegate
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "we have to parse " + inputBytes.length + " bytes");
      }
    }
    return e;
  }

  private static X509Certificate getRawCertificate(XMLSignatureInput resource)
      throws CanonicalizationException, IOException, CertificateException {
    byte inputBytes[] = resource.getBytes();
    // if the resource stores a raw certificate, we have to handle it
    CertificateFactory certFact =
        CertificateFactory.getInstance(XMLX509Certificate.JCA_CERT_ID);
    X509Certificate cert = (X509Certificate)
        certFact.generateCertificate(new ByteArrayInputStream(inputBytes));
    return cert;
  }

  /**
   * Resolves the input from the given retrieval method
   */
  private static XMLSignatureInput resolveInput(
      RetrievalMethod rm, String baseURI, boolean secureValidation
  ) throws XMLSecurityException {
    Attr uri = rm.getURIAttr();
    // Apply the transforms
    Transforms transforms = rm.getTransforms();
    ResourceResolver resRes = ResourceResolver.getInstance(uri, baseURI, secureValidation);
    XMLSignatureInput resource = resRes.resolve(uri, baseURI, secureValidation);
    if (transforms != null) {
      if (log.isLoggable(java.util.logging.Level.FINE)) {
        log.log(java.util.logging.Level.FINE, "We have Transforms");
      }
      resource = transforms.performTransforms(resource);
    }
    return resource;
  }

  /**
   * Parses a byte array and returns the parsed Element.
   *
   * @return the Document Element after parsing bytes
   * @throws KeyResolverException if something goes wrong
   */
  private static Element getDocFromBytes(byte[] bytes) throws KeyResolverException {
    try {
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      dbf.setNamespaceAware(true);
      dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.parse(new ByteArrayInputStream(bytes));
      return doc.getDocumentElement();
    } catch (SAXException ex) {
      throw new KeyResolverException("empty", ex);
    } catch (IOException ex) {
      throw new KeyResolverException("empty", ex);
    } catch (ParserConfigurationException ex) {
      throw new KeyResolverException("empty", ex);
    }
  }

  /**
   * Method engineResolveSecretKey
   *
   * @inheritDoc
   */
  public javax.crypto.SecretKey engineLookupAndResolveSecretKey(
      Element element, String baseURI, StorageResolver storage
  ) {
    return null;
  }

  private static Element getDocumentElement(Set<Node> set) {
    Iterator<Node> it = set.iterator();
    Element e = null;
    while (it.hasNext()) {
      Node currentNode = it.next();
      if (currentNode != null && Node.ELEMENT_NODE == currentNode.getNodeType()) {
        e = (Element) currentNode;
        break;
      }
    }
    List<Node> parents = new ArrayList<Node>();

    // Obtain all the parents of the elemnt
    while (e != null) {
      parents.add(e);
      Node n = e.getParentNode();
      if (n == null || Node.ELEMENT_NODE != n.getNodeType()) {
        break;
      }
      e = (Element) n;
    }
    // Visit them in reverse order.
    ListIterator<Node> it2 = parents.listIterator(parents.size() - 1);
    Element ele = null;
    while (it2.hasPrevious()) {
      ele = (Element) it2.previous();
      if (set.contains(ele)) {
        return ele;
      }
    }
    return null;
  }
}
